package se.webbzon.boltzmann.game.object.dialogue;

import java.awt.Dimension;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import javax.media.j3d.Transform3D;
import javax.vecmath.Vector3d;

import se.webbzon.boltzmann.debug.Debug;
import se.webbzon.boltzmann.environment.AbstractEnvironment;
import se.webbzon.boltzmann.game.object.dialogue.layout.DefaultDialogueLayout;
import se.webbzon.boltzmann.game.object.dialogue.layout.DialogueLayout;
import se.webbzon.boltzmann.game.object.dialogue.word.AbstractDialogueWord;
import se.webbzon.boltzmann.game.sequence.Sequence;
import se.webbzon.boltzmann.input.InputListener;
import se.webbzon.boltzmann.resource.Resources;
import se.webbzon.oschi01.worldarea.WorldArea;
import se.webbzon.oschi01.worldobject.WorldObject;
import se.webbzon.oschi01.worldobject.WorldStaticObject;
import se.webbzon.oschi01.worldobject.appearance.AbstractAppearance3D;
import se.webbzon.oschi01.worldobject.appearance.Appearance3D;
import se.webbzon.oschi01.worldobject.appearance.GroupAppearance3D;
import se.webbzon.oschi01.worldobject.appearance.SimpleAppearance3D;
import se.webbzon.oschi01.worldobject.appearance.SimpleGroupAppearance3D;
import se.webbzon.oschi01.worldobject.appearance.XYPlaneAppearance3D;

public abstract class AbstractDialogue extends WorldStaticObject implements InputListener {

	// The default layout of an abstract dialogue
	private static final DialogueLayout defaultLayout = new DefaultDialogueLayout();
	
	// The layout of the dialogue
	private final DialogueLayout layout;
	
	// A collection of all listeners to this dialogue
	private final Collection<DialogueListener> listeners;
	
	// Whatever this dialogue is spoken or not
	private final boolean isSpoken;
	
	// The abstract environment associated with this dialogue
	private AbstractEnvironment env;
	
	// The sequence associated with this dialogue
	private Sequence sequence;
	
	// The talker associated with the dialogue
	private Talker talker;
	
	// A list of all words in the dialogue
	private List<AbstractDialogueWord> words;
	
	// The current word and character to display
	private int currentWord, currentChar;
	
	// Whatever this dialogue is paused or not
	private boolean paused, hidden;
	
	// Whatever the dialogue has notified the listeners
	private boolean notifiedShowAll;
	
	/*============================================================
	Constructors
	============================================================*/
	
	/** Creates a new abstract dialogue. The words in the dialogue 
	 * will be parsed from the given text. A default layout will be 
	 * used for the abstract dialogue. **/
	public AbstractDialogue(String text) {
		this(DialogueBackground.NORMAL,text);
	}
	
	/** Creates a new abstract dialogue with a given background. 
	 * The words in the dialogue will be parsed from the given text. 
	 * A default layout will be used for the abstract dialogue. **/
	public AbstractDialogue(DialogueBackground background, String text) {
		this(defaultLayout,background,text);
	}
	
	/** Creates a new abstract dialogue with a given background. 
	 * The words in the dialogue will be parsed from the given text. 
	 * A dialogue layout must be provided. **/
	public AbstractDialogue(DialogueLayout layout, 
							DialogueBackground background,
							String text) {
		this(layout,background,DialogueTextParser.parse(layout,text));
	}
	
	/** Creates a new abstract dialogue with a given list of words. 
	 * A default layout will be used for the abstract dialogue. **/
	public AbstractDialogue(List<AbstractDialogueWord> words) {
		this(DialogueBackground.NORMAL,words);
	}
	
	/** Creates a new abstract dialogue with a given list of words
	 * and a background. A default layout will be used for the 
	 * abstract dialogue. **/
	public AbstractDialogue(DialogueBackground background,
							List<AbstractDialogueWord> words) {
		this(defaultLayout,background,words);
	}
	
	/** Creates a new abstract dialogue with a given list of words.
	 * A layout and a background must be provided. **/
	public AbstractDialogue(DialogueLayout layout,
							DialogueBackground background,
							List<AbstractDialogueWord> words) {
		super(WorldObject.onCreate + WorldObject.onStep + WorldObject.onDestroy,
				0, createDialogue(layout,background,words));
		
		this.layout = layout;
		this.words = words;
		currentWord = 0;
		currentChar = 0;
		paused = true;
		hidden = true;
		listeners = new ArrayList<DialogueListener>();	
		isSpoken = (background == DialogueBackground.WEAK ||
					background == DialogueBackground.NORMAL ||
					background == DialogueBackground.STRONG);
				
		setVisible(false);
	}
	
	/*============================================================
	Public Methods
	============================================================*/
	
	
	/** This method is invoked when the input for the dialogue is
	 * about to be showed. **/
	public abstract void onShowInput();
	
	/** This method is invoked when the input for the dialogue is
	 * about to be hidden. **/
	public abstract void onHideInput();
	
	/** Adds a dialogue listener to this dialogue. **/
	public void addListener(DialogueListener listener) {
		int d;
		Debug.debugCollection(this, listeners);
		listeners.add(listener);
	}
	
	/** Removes a dialogue listener from this dialogue. **/
	public void removeListener(DialogueListener listener) {
		listeners.remove(listener);
	}
	
	/** Removes all dialogue listeners from this dialogue. **/
	public void clearListeners() {
		listeners.clear();
	}
	
	/** Returns the abstract environment of this dialogue. **/
	public AbstractEnvironment getEnvironment() {
		return env;
	}
	
	/** Returns the sequence associated with this dialogue. **/
	public Sequence getSequence() {
		return sequence;
	}
	
	/** Returns the talker of this dialogue. **/
	public Talker getTalker() {
		return talker;
	}
	
	/** Returns true if the dialogue is spoken. **/
	public boolean isSpoken() {
		return isSpoken;
	}
	
	/** Returns true if the dialogue is active. **/
	public boolean isPaused() {
		return paused;
	}
	
	/** Returns true if the dialogue was displayed and the parent of 
	 * the dialogue was set to the given talker. If the talker is null 
	 * or if the dialogue was already displaying then false is returned 
	 * and the state of the dialogue will remain unchanged. **/
	public boolean display(WorldStaticObject talker) {
		if (talker == null || !paused)
			return false;
		else {
			// Activate the dialogue
			if (talker instanceof Talker)
				this.talker = (Talker) talker;
			else
				this.talker = new TalkerWrapper(talker);
			notifiedShowAll = false;
			paused = false;
			hidden = false;
			updatePosition();
			updateLayout();
			setVisible(true);
			return true;
		}
	}
	
	/** Returns true if the dialogue was displayed and the parent of 
	 * the dialogue was set to the given talker. If the talker is null 
	 * or if the dialogue was already displaying then false is returned 
	 * and the state of the dialogue will remain unchanged. An abstract
	 * environment may also be provided when calling this method which
	 * can be used to print dynamic dialogue words. Some dialogue types 
	 * may also require an abstract environment. **/
	public boolean display(AbstractEnvironment env, WorldStaticObject talker) {
		this.env = env;
		return display(talker);
	}
	
	/** Returns true if the dialogue was displayed and the parent of 
	 * the dialogue was set to the given talker. If the talker is null 
	 * or if the dialogue was already displaying then false is returned 
	 * and the state of the dialogue will remain unchanged. A sequence 
	 * may also be provided when calling this method which can be used 
	 * to print dynamic words. Some dialogue types may also require a
	 * provided sequence. The abstract environment of the sequence is
	 * automatically bound to the dialogue when calling this method. **/
	public boolean display(Sequence sequence, WorldStaticObject talker) {
		this.sequence = sequence;
		return display(sequence.getEnvironment(),talker);
	}
	
	/** Returns true if the dialogue was paused. If the dialogue was
	 * already paused or if it is hidden then false is returned. **/
	public boolean pause() {
		if (paused || hidden)
			return false;
		else {
			paused = true;
			return true;
		}
	}
	
	/** Returns true if the dialogue was resumed. If the dialogue was
	 * not paused or if it is hidden then false is returned. **/
	public boolean resume() {
		if (paused && !hidden) {
			paused = false;
			return true;
		} else
			return false;
	}
	
	/** Hides and resets the dialogue and all words associated with 
	 * the dialogue. Calling the reset method will invoke the methods 
	 * pause() and onHideInput(). This method should NOT reset the 
	 * selection of the dialogue, this should only be performed when 
	 * then method onShowInput() is called. **/
	public void hide() {
		for (AbstractDialogueWord word : words)
			word.setCharacter(this, 0);
		currentWord = 0;
		currentChar = 0;
		hidden = true;
		paused = true;
		pause();
		onHideInput();
		setVisible(false);
	}
	
	/** Returns true if the dialogue is ready for input. If this method 
	 * return false then the input should be ignored. **/
	public boolean isInputReady() {
		if (words.size() == 0)
			return true;
		else {
			AbstractDialogueWord word = words.get(0);
			if (word.getWord().length() == 0)
				return true;
			else
				return word.getCharacter() > 0;
		}
	}
	
	/** Returns true if all the words in the dialogue has been printed. **/
	public boolean isAllVisible() {
		return words.get(words.size()-1).isAllVisible();
	}
	
	/** By calling this method, all words in the dialogue will be shown
	 * and the method showInput will be invoked. **/
	public void showAllWords() {
		for (int i = currentWord; i < words.size(); i++)
			words.get(i).showAllCharacters(this);
		
		if (!notifiedShowAll) {
			notifiedShowAll = true;
			// Show input and notify listeners
			for (DialogueListener listener : listeners.toArray(new DialogueListener[0]))
				listener.onShowInput(this);
			onShowInput();
		}
	}
	
	/** Returns a list of all words used in this dialogue. **/
	public List<AbstractDialogueWord> getWords() {
		return Collections.unmodifiableList(words);
	}
	
	/** Returns the layout of the dialogue. **/
	public DialogueLayout getLayout() {
		return layout;
	}
	
	@Override public void setVisible(boolean visible) {
		if (visible && !paused) 
			updatePosition();
		super.setVisible(visible);
	}
	
	@Override public void onStep(WorldArea world) {
		if (!paused) {
			updatePosition();
			
			final AbstractDialogueWord word = words.get(currentWord);
			if (!word.isAllVisible()) {
				final int position = word.getCharacter();
				final int length = word.getWord().length();
				// Play a sound on each second letter
				if (position % 2 == 0)
					talker.getVoice().playRandom();
				
				currentChar += layout.getCharactersPerStep();
				if (currentChar >= length) {
					word.showAllCharacters(this);
					currentChar = 0;
				} else {
					word.setCharacter(this, (int) currentChar);
				}
					
			} else if (currentWord < words.size()-1) {
				currentWord++;
				//final DialogueWord nextWord = words.get(currentWord);
				//nextWord.setActive(true);
				
			} else if (!notifiedShowAll) {
				notifiedShowAll = true;
				// Show input and notify listeners
				for (DialogueListener listener : listeners.toArray(new DialogueListener[0]))
					listener.onShowInput(this);
				onShowInput();
			}
		}
	}
	
	/*============================================================
	Protected Methods
	============================================================*/
	
	/** Returns a collection of all listeners to this dialogue. **/
	public Collection<DialogueListener> getListeners() {
		return Collections.unmodifiableCollection(listeners);
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Updates the position of the dialogue. **/
	private void updatePosition() {
		final double dialogueX = talker.getX() + layout.getXOffset();
		final double dialogueY = talker.getY() + layout.getYOffset() + talker.getHeight()/2;
		final double dialogueZ = talker.getZ();
		setPosition(dialogueX,dialogueY,dialogueZ);
	}
	
	/** Updates the layout of the dialogue. **/
	private void updateLayout() {		
		// Calculate the offset of all words in the dialogue
		Dimension d = new Dimension();
		double x = - layout.getWidth()/2 + layout.getTextXOffset(); 
		double y = layout.getHeight()/2 - layout.getTextYOffset();	
		double h = 0;
		
		AbstractDialogueWord lastWord = null;
		for (AbstractDialogueWord word : words) {
			word.onUpdateWord(this);
			if (lastWord != null) {
				lastWord.getDimension(d);
				if (lastWord.isNewLine()) {
					x = -layout.getWidth()/2+layout.getTextXOffset();
					y -= h + layout.getSpacing();
					h = 0;
				} else {
					x += d.getWidth();
					h = Math.max(h, d.getHeight());
				}
			}
			word.setPosition(x,y,0);
			lastWord = word;
		}
	}
	
	/** Creates a new dialogue appearance. **/
	private static Appearance3D createDialogue(	DialogueLayout layout, 
												DialogueBackground background,
												List<AbstractDialogueWord> words) {
		final ArrayList<Appearance3D> appearances = new ArrayList<Appearance3D>();
		final Transform3D transform = new Transform3D();
		final Vector3d vector = new Vector3d();
		
		// Create the dialogue box
		final XYPlaneAppearance3D dialogue = 
				new XYPlaneAppearance3D(0, SimpleAppearance3D.TRANSPARENCY, false);
		dialogue.setTextureSet(Resources.getTextureSet("misc_dialogue"));
		dialogue.setTextureIndex(background.ordinal());
		vector.set(layout.getWidth(),layout.getHeight(),0);
		transform.setScale(vector);
		dialogue.setTransform(transform);
		appearances.add(dialogue);
		
		// Add all the words to the dialogue
		for (AbstractDialogueWord word : words)
			appearances.add(word.getAppearance());
		
		final SimpleGroupAppearance3D appearance = 
				new SimpleGroupAppearance3D(0,	
											GroupAppearance3D.TRANSPARENCY,// + 
												//GroupAppearance3D.EXTENDING,
											appearances);
		return appearance;
	}

}
